by Neel Shah
The import statements and setup functions required to run this program are located below.
import warnings
warnings.filterwarnings("ignore")
import ipywidgets as widgets
from ipywidgets import interact, interact_manual
from IPython.display import Javascript, display
import ipython_blocking
from datetime import datetime, timedelta
import numpy as np
import pandas as pd
import pandas_datareader.data as web
import talib
from talib.abstract import *
import matplotlib.pyplot as plt
%matplotlib inline
import plotly.offline as py
py.init_notebook_mode(connected = False)
import plotly.express as px
import plotly.graph_objects as go
from plotly.subplots import make_subplots
Below, the user should input a stock ticker symbol (the code used to identify a stock or cryptocurrency). Some good examples are:
The code will resume once the Start button is clicked.
ticker = widgets.Text(description = 'Stock Ticker')
button = widgets.Button(description = 'Start')
box = widgets.VBox(children = [ticker, button])
box
%blockrun button
The code below uses pandas-datareader to get information about the selected stock from Yahoo Finance.
stock = web.DataReader(ticker.value, 'yahoo').reset_index()
stock = round(stock, 2)
stock['Volume'] = stock['Volume'].round(0).astype(int)
stock
| Date | High | Low | Open | Close | Volume | Adj Close | |
|---|---|---|---|---|---|---|---|
| 0 | 2015-02-10 | 184.80 | 182.35 | 183.35 | 184.56 | 2556300 | 171.20 |
| 1 | 2015-02-11 | 188.19 | 183.75 | 184.07 | 187.65 | 4079600 | 174.07 |
| 2 | 2015-02-12 | 190.00 | 187.40 | 188.25 | 189.78 | 3086700 | 176.04 |
| 3 | 2015-02-13 | 191.33 | 188.34 | 189.87 | 189.00 | 2720400 | 175.32 |
| 4 | 2015-02-17 | 190.63 | 188.31 | 188.78 | 190.02 | 2123300 | 176.27 |
| ... | ... | ... | ... | ... | ... | ... | ... |
| 1253 | 2020-02-03 | 242.39 | 238.05 | 238.36 | 239.01 | 2733000 | 239.01 |
| 1254 | 2020-02-04 | 243.74 | 241.55 | 242.88 | 241.94 | 3052500 | 241.94 |
| 1255 | 2020-02-05 | 245.13 | 243.00 | 244.99 | 244.30 | 3126600 | 244.30 |
| 1256 | 2020-02-06 | 245.77 | 241.18 | 245.35 | 241.82 | 2225600 | 241.82 |
| 1257 | 2020-02-07 | 240.52 | 236.55 | 239.75 | 238.00 | 3006500 | 238.00 |
1258 rows × 7 columns
The code below calculates the simple moving average for the stock price over the past 200 days (SMA200) and past 50 days (SMA50).
stock['SMA200'] = talib.SMA(stock['Close'], timeperiod = 200)
stock['SMA50'] = talib.SMA(stock['Close'], timeperiod = 50)
stock['EMA26'] = talib.EMA(stock['Close'], timeperiod = 26)
stock['EMA12'] = talib.EMA(stock['Close'], timeperiod = 12)
stock['EMA9'] = talib.EMA(stock['Close'], timeperiod = 9)
stock['MACD'], stock['MACDSignal'], stock['MACDHist'] = talib.MACD(stock['Close'], fastperiod = 12, slowperiod = 26, signalperiod = 9)
stock['MACDHist_SMA'] = talib.SMA(stock['MACDHist'], timeperiod = 10)
The code below calculates the relative strength index using a 14 day period. Relative sterength is calculated by doing: $RS=\frac{avg. gain}{avg. loss}$. RSI then uses the following formula: $RSI=100-\frac{100}{1+RS}$.
stock['RSI'] = talib.RSI(stock['Close'], timeperiod = 14)
stock['RSI_SMA'] = talib.SMA(stock['RSI'], timeperiod = 10)
The on-balance volume indicator measures cumulative buying/selling pressure by adding the volume on up days and subtracting volume on down days. It can be considered a running total of the stock's trading volume.
stock['OBV'] = talib.OBV(stock['Close'], stock['Volume']).round(0).astype(int)
stock['OBV_SMA'] = talib.SMA(stock['OBV'], timeperiod = 10)
The code below displays the retrieved stock data and all of the calculations performed.
stock = round(stock, 2)
stock
| Date | High | Low | Open | Close | Volume | Adj Close | SMA200 | SMA50 | EMA26 | EMA12 | EMA9 | MACD | MACDSignal | MACDHist | MACDHist_SMA | RSI | RSI_SMA | OBV | OBV_SMA | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 2015-02-10 | 184.80 | 182.35 | 183.35 | 184.56 | 2556300 | 171.20 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | 2556300 | NaN |
| 1 | 2015-02-11 | 188.19 | 183.75 | 184.07 | 187.65 | 4079600 | 174.07 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | 6635900 | NaN |
| 2 | 2015-02-12 | 190.00 | 187.40 | 188.25 | 189.78 | 3086700 | 176.04 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | 9722600 | NaN |
| 3 | 2015-02-13 | 191.33 | 188.34 | 189.87 | 189.00 | 2720400 | 175.32 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | 7002200 | NaN |
| 4 | 2015-02-17 | 190.63 | 188.31 | 188.78 | 190.02 | 2123300 | 176.27 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | 9125500 | NaN |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 1253 | 2020-02-03 | 242.39 | 238.05 | 238.36 | 239.01 | 2733000 | 239.01 | 212.15 | 231.87 | 238.99 | 241.35 | 241.22 | 2.36 | 3.76 | -1.40 | -0.55 | 50.37 | 57.85 | 22441200 | 23373980.0 |
| 1254 | 2020-02-04 | 243.74 | 241.55 | 242.88 | 241.94 | 3052500 | 241.94 | 212.33 | 232.35 | 239.21 | 241.44 | 241.37 | 2.23 | 3.46 | -1.22 | -0.75 | 54.27 | 56.47 | 25493700 | 23255610.0 |
| 1255 | 2020-02-05 | 245.13 | 243.00 | 244.99 | 244.30 | 3126600 | 244.30 | 212.53 | 232.87 | 239.59 | 241.88 | 241.95 | 2.30 | 3.22 | -0.93 | -0.89 | 57.19 | 55.23 | 28620300 | 23246810.0 |
| 1256 | 2020-02-06 | 245.77 | 241.18 | 245.35 | 241.82 | 2225600 | 241.82 | 212.72 | 233.30 | 239.75 | 241.87 | 241.93 | 2.12 | 3.00 | -0.88 | -1.00 | 53.34 | 53.98 | 26394700 | 23296130.0 |
| 1257 | 2020-02-07 | 240.52 | 236.55 | 239.75 | 238.00 | 3006500 | 238.00 | 212.91 | 233.61 | 239.62 | 241.28 | 241.14 | 1.66 | 2.73 | -1.08 | -1.08 | 47.98 | 53.03 | 23388200 | 23332540.0 |
1258 rows × 20 columns
fig = go.Figure(data = [go.Candlestick(x = stock['Date'],
open = stock['Open'],
high = stock['High'],
low = stock['Low'],
close = stock['Close'])])
fig.update_layout(title_text = 'Candlestick',
xaxis_title = 'Date',
yaxis_title = 'Price',
xaxis_rangeslider_visible = True)
fig.update_yaxes(nticks = 10)
fig.show()
The interactive line graph below shows the closing price, the 200-day simple moving average, and the 50-day simple moving average for the stock. Moving averages show what is hapenning to the price of a stock (on average) over time:
fig = go.Figure()
fig.add_trace(go.Scatter(x = stock['Date'], y = stock['Close'], name = 'Closing Price'))
fig.add_trace(go.Scatter(x = stock['Date'], y = stock['SMA200'], name = '200-Day Simple Moving Average'))
fig.add_trace(go.Scatter(x = stock['Date'], y = stock['SMA50'], name = '50-Day Simple Moving Average'))
fig.update_layout(title_text = 'Simple Moving Average (SMA) Crossovers',
xaxis_title = 'Date',
yaxis_title = 'Price',
xaxis_rangeslider_visible = True)
fig.update_yaxes(nticks = 10)
fig.show()
The interactive graph below shows the moving average convergence divergence (MACD), signal line (MACDSignal), and histogram (MACDHist).
A buy signal occurs when:
A sell signal occurs when:
The histogram serves as a warning, not a signal:
fig = make_subplots(rows = 2, cols = 1, shared_xaxes = True)
fig.add_trace(go.Scatter(x = stock['Date'], y = stock['Close'], name = 'Closing Price'), row = 1, col = 1)
fig.add_trace(go.Scatter(x = stock['Date'], y = pd.Series([0]).repeat(len(stock)), showlegend = False, hoverinfo = 'skip', line = dict(color = 'black', dash = 'dot')), row = 2, col = 1)
fig.add_trace(go.Scatter(x = stock['Date'], y = stock['MACD'], name = 'Moving Average Convergence Divergence'), row = 2, col = 1)
fig.add_trace(go.Scatter(x = stock['Date'], y = stock['MACDSignal'], name = 'MACD Signal Line'), row = 2, col = 1)
fig.add_trace(go.Bar(x = stock['Date'], y = stock['MACDHist'], name = 'MACD Histogram', marker_color = 'black'), row = 2, col = 1)
fig.update_layout(title_text = 'Moving Average Convergence Divergence (MACD)', height = 800)
fig.update_xaxes(title_text = 'Date', row = 2, col = 1)
fig.update_yaxes(title_text = 'Price', row = 1, col = 1)
fig.update_yaxes(title_text = 'MACD', row = 2, col = 1)
fig.update_yaxes(nticks = 10, row = 1, col = 1)
fig.update_yaxes(nticks = 10, row = 2, col = 1)
fig.update_yaxes(fixedrange = True, row = 1, col = 1)
fig.update_yaxes(fixedrange = True, row = 2, col = 1)
fig.show()
The interactive line graph below shows the relative strength index (RSI) for the stock. RSI values can indicate whether or not a stock is due for a correction:
In a broader sense:
fig = make_subplots(rows = 2, cols = 1, shared_xaxes = True)
fig.add_trace(go.Scatter(x = stock['Date'], y = stock['Close'], name = 'Closing Price'), row = 1, col = 1)
fig.add_trace(go.Scatter(x = stock['Date'], y = stock['RSI'], name = 'Relative Strength Index', line = dict(color = 'purple')), row = 2, col = 1)
fig.add_trace(go.Scatter(x = stock['Date'], y = pd.Series([50]).repeat(len(stock)), showlegend = False, hoverinfo = 'skip', line = dict(color = 'black', dash = 'dot')), row = 2, col = 1)
fig.add_trace(go.Scatter(x = stock['Date'], y = pd.Series([30]).repeat(len(stock)), fill = 'tozeroy', showlegend = False, hoverinfo = 'skip', line = dict(color = 'green', dash = 'dash')), row = 2, col = 1)
fig.add_trace(go.Scatter(x = stock['Date'], y = pd.Series([70]).repeat(len(stock)), showlegend = False, hoverinfo = 'skip', line = dict(color = 'red', dash = 'dash')), row = 2, col = 1)
fig.add_trace(go.Scatter(x = stock['Date'], y = pd.Series([100]).repeat(len(stock)), fill = 'tonexty', showlegend = False, hoverinfo = 'skip', line = dict(color = 'red')), row = 2, col = 1)
fig.update_layout(title_text = 'Relative Strength Index (RSI)')
fig.update_xaxes(title_text = 'Date', row = 2, col = 1)
fig.update_yaxes(title_text = 'Price', row = 1, col = 1)
fig.update_yaxes(title_text = 'Relative Strength Index', row = 2, col = 1)
fig.update_yaxes(nticks = 10, row = 1, col = 1)
fig.update_yaxes(nticks = 10, row = 2, col = 1)
fig.update_yaxes(fixedrange = True, row = 1, col = 1)
fig.update_yaxes(fixedrange = True, row = 2, col = 1)
fig.show()
The interactive line graph below shows the on-balance volume (OBV) for the stock. The OBV typically confirms trends:
Thus, the stock price tends to follow the direction of the on-balance volume graph.
fig = make_subplots(rows = 2, cols = 1, shared_xaxes = True)
fig.add_trace(go.Scatter(x = stock['Date'], y = stock['Close'], name = 'Closing Price'), row = 1, col = 1)
fig.add_trace(go.Scatter(x = stock['Date'], y = stock['OBV'], name = 'On-Balance Volume'), row = 2, col = 1)
fig.update_layout(title_text = 'On-Balance Volume (OBV)')
fig.update_xaxes(title_text = 'Date', row = 2, col = 1)
fig.update_yaxes(title_text = 'Price', row = 1, col = 1)
fig.update_yaxes(title_text = 'On-Balance Volume', row = 2, col = 1)
fig.update_yaxes(nticks = 10, row = 1, col = 1)
fig.update_yaxes(nticks = 10, row = 2, col = 1)
fig.update_yaxes(fixedrange = True, row = 1, col = 1)
fig.update_yaxes(fixedrange = True, row = 2, col = 1)
fig.show()
The code below develops an investment strategy based on the stock market indicators listed above:
According to their values, each indicator is assigned a score ranging from -1 to 1. The total score is calculated and if it is greater than or equal to 2, the stock is given a buy rating. If the total is less than or equal to -2, the stock is given a sell rating. Otherwise, the stock is given a hold rading.
In addition, the code uses these ratings to simulate the purchase and sale of stock shares given an initial $1 million investment. The strategy is compared to a buy and hold strategy, in which the investor does not make any trades following an initial investment.
stock['Cash'] = pd.Series([0]).repeat(len(stock)).values
stock['Cash'][0] = 1000000
stock['Shares'] = pd.Series([0]).repeat(len(stock)).values
stock['Custom Strategy'] = pd.Series([1000000]).repeat(len(stock)).values
shares_buy_and_hold = stock['Cash'][0] // stock['Close'][0]
cash_buy_and_hold = stock['Cash'][0] - (shares_buy_and_hold * stock['Close'][0])
stock['Buy and Hold Strategy'] = pd.Series([1000000]).repeat(len(stock)).values
def strategy(i):
if i > 0:
stock['Cash'][i] = stock['Cash'][i - 1]
stock['Shares'][i] = stock['Shares'][i - 1]
stock['Custom Strategy'][i] = stock['Cash'][i] + stock['Shares'][i] * stock['Close'][i]
stock['Buy and Hold Strategy'][i] = cash_buy_and_hold + shares_buy_and_hold * stock['Close'][i]
info = stock.loc[i].squeeze()
date = info['Date']
indicators = {'Date' : date, 'SMA' : 0.0, 'MACD' : 0.0, 'RSI' : 0.0, 'OBV' : 0.0, 'Total' : 0.0, 'Rating' : 'Hold'}
if info['SMA50'] > info['SMA200']:
indicators['SMA'] += 1.0
elif info['SMA50'] < info['SMA200']:
indicators['SMA'] -= 1.0
if info['MACD'] > 0:
indicators['MACD'] += 2/3
elif info['MACD'] < 0:
indicators['MACD'] -= 2/3
if info['MACD'] > info['MACDSignal']:
indicators['MACD'] += 1/3
elif info['MACD'] < info['MACDSignal']:
indicators['MACD'] -= 1/3
if info['MACDHist'] > 0 and info['MACDHist'] < info['MACDHist_SMA']:
indicators['MACD'] -= 1/3
elif info['MACDHist'] < 0 and info['MACDHist'] >info['MACDHist_SMA']:
indicators['MACD'] += 1/3
if info['RSI'] < 30:
indicators['RSI'] += 1.0
elif info['RSI'] > 70:
indicators['RSI'] -= 1.0
elif info['RSI_SMA'] < 50 and info['RSI'] > 50:
indicators['RSI'] += 2/3
elif info['RSI_SMA'] > 50 and info['RSI'] < 50:
indicators['RSI'] -= 2/3
if info['OBV'] > info['OBV_SMA']:
indicators['OBV'] += 1.0
elif info['OBV'] < info['OBV_SMA']:
indicators['OBV'] -= 1.0
indicators['Total'] = indicators['SMA'] + indicators['MACD'] + indicators['RSI'] + indicators['OBV']
if indicators['Total'] >= 2.0:
indicators['Rating'] = 'Buy'
elif indicators['Total'] <= -2.0:
indicators['Rating'] = 'Sell'
if indicators['Rating'] == 'Buy':
shares = info['Cash'] // info['Close']
cash = shares * info['Close']
if stock['Cash'][i] - cash >= 0:
stock['Cash'][i] -= cash
stock['Shares'][i] += shares
elif indicators['Rating'] == 'Sell':
shares = info['Shares']
cash = shares * info['Close']
if stock['Shares'][i] - shares >= 0:
stock['Cash'][i] += cash
stock['Shares'][i] -= shares
return indicators
The code below uses the strategy documented above to indicate whether an investor should buy, sell, or hold the stock.
print(strategy(len(stock) - 1)['Rating'])
Hold
The code below implements the simulation described above using stock market data from the past 5 years to see the results of the strategy given a $1 million investment.
for i in range(len(stock)):
strategy(i)
stock
| Date | High | Low | Open | Close | Volume | Adj Close | SMA200 | SMA50 | EMA26 | ... | MACDHist | MACDHist_SMA | RSI | RSI_SMA | OBV | OBV_SMA | Cash | Shares | Custom Strategy | Buy and Hold Strategy | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 2015-02-10 | 184.80 | 182.35 | 183.35 | 184.56 | 2556300 | 171.20 | NaN | NaN | NaN | ... | NaN | NaN | NaN | NaN | 2556300 | NaN | 1000000 | 0 | 1000000 | 1000000 |
| 1 | 2015-02-11 | 188.19 | 183.75 | 184.07 | 187.65 | 4079600 | 174.07 | NaN | NaN | NaN | ... | NaN | NaN | NaN | NaN | 6635900 | NaN | 1000000 | 0 | 1000000 | 1016741 |
| 2 | 2015-02-12 | 190.00 | 187.40 | 188.25 | 189.78 | 3086700 | 176.04 | NaN | NaN | NaN | ... | NaN | NaN | NaN | NaN | 9722600 | NaN | 1000000 | 0 | 1000000 | 1028281 |
| 3 | 2015-02-13 | 191.33 | 188.34 | 189.87 | 189.00 | 2720400 | 175.32 | NaN | NaN | NaN | ... | NaN | NaN | NaN | NaN | 7002200 | NaN | 1000000 | 0 | 1000000 | 1024055 |
| 4 | 2015-02-17 | 190.63 | 188.31 | 188.78 | 190.02 | 2123300 | 176.27 | NaN | NaN | NaN | ... | NaN | NaN | NaN | NaN | 9125500 | NaN | 1000000 | 0 | 1000000 | 1029582 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 1253 | 2020-02-03 | 242.39 | 238.05 | 238.36 | 239.01 | 2733000 | 239.01 | 212.15 | 231.87 | 238.99 | ... | -1.40 | -0.55 | 50.37 | 57.85 | 22441200 | 23373980.0 | 133 | 5963 | 1425349 | 1295010 |
| 1254 | 2020-02-04 | 243.74 | 241.55 | 242.88 | 241.94 | 3052500 | 241.94 | 212.33 | 232.35 | 239.21 | ... | -1.22 | -0.75 | 54.27 | 56.47 | 25493700 | 23255610.0 | 133 | 5963 | 1442821 | 1310884 |
| 1255 | 2020-02-05 | 245.13 | 243.00 | 244.99 | 244.30 | 3126600 | 244.30 | 212.53 | 232.87 | 239.59 | ... | -0.93 | -0.89 | 57.19 | 55.23 | 28620300 | 23246810.0 | 133 | 5963 | 1456893 | 1323671 |
| 1256 | 2020-02-06 | 245.77 | 241.18 | 245.35 | 241.82 | 2225600 | 241.82 | 212.72 | 233.30 | 239.75 | ... | -0.88 | -1.00 | 53.34 | 53.98 | 26394700 | 23296130.0 | 133 | 5963 | 1442105 | 1310234 |
| 1257 | 2020-02-07 | 240.52 | 236.55 | 239.75 | 238.00 | 3006500 | 238.00 | 212.91 | 233.61 | 239.62 | ... | -1.08 | -1.08 | 47.98 | 53.03 | 23388200 | 23332540.0 | 133 | 5963 | 1419327 | 1289537 |
1258 rows × 24 columns
The code below creates an interactive graph comparing your portfolio value when using:
fig = go.Figure()
fig.add_trace(go.Scatter(x = stock['Date'], y = stock['Custom Strategy'], name = 'Custom Strategy'))
fig.add_trace(go.Scatter(x = stock['Date'], y = stock['Buy and Hold Strategy'], name = 'Buy and Hold Strategy'))
fig.add_trace(go.Scatter(x = stock['Date'], y = pd.Series([1000000]).repeat(len(stock)), showlegend = False, hoverinfo = 'skip', line = dict(color = 'black', dash = 'dot')))
fig.update_layout(title_text = 'Portfolio Value',
xaxis_title = 'Date',
yaxis_title = 'Portfolio Value',
xaxis_rangeslider_visible = True,
showlegend = True)
fig.update_yaxes(nticks = 10)
fig.show()